// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.hints

import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInsight.ExternalAnnotationsManager
import com.intellij.codeInsight.InferredAnnotationsManager
import com.intellij.codeInsight.MakeInferredAnnotationExplicit
import com.intellij.codeInsight.daemon.impl.InlayHintsPassFactoryInternal
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.MenuOnClickPresentation
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.codeInsight.hints.presentation.SequencePresentation
import com.intellij.codeInsight.javadoc.JavaDocInfoGenerator
import com.intellij.codeInspection.dataFlow.Mutability
import com.intellij.java.JavaBundle
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.editor.BlockInlayPriority
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsActions
import com.intellij.pom.java.JavaFeature
import com.intellij.psi.*
import com.intellij.psi.tree.TokenSet
import com.intellij.psi.util.PsiUtil
import com.intellij.ui.dsl.builder.panel
import java.awt.Font
import javax.swing.JComponent
import kotlin.reflect.KMutableProperty0

class AnnotationInlayProvider : InlayHintsProvider<AnnotationInlayProvider.Settings> {

  override val group: InlayGroup
    get() = InlayGroup.ANNOTATIONS_GROUP

  private val arrayTypeStart = TokenSet.create(JavaTokenType.LBRACKET, JavaTokenType.ELLIPSIS)

  override fun getCollectorFor(file: PsiFile,
                               editor: Editor,
                               settings: Settings,
                               sink: InlayHintsSink): InlayHintsCollector {
    val project = file.project
    val document = PsiDocumentManager.getInstance(project).getDocument(file)
    return object : FactoryInlayHintsCollector(editor) {
      override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
        if (DumbService.getInstance(file.project).isDumb) return false
        if (file.project.isDefault) return false
        if (element is PsiTypeElement && settings.showExternal) {
          val originalElement = element.originalElement
          if (originalElement is PsiTypeElement && originalElement is PsiCompiledElement) {
            val type = originalElement.type
            collectTypeAnnotations(type, element, sink)
          }
        }
        else if (element is PsiModifierListOwner) {
          var annotations = emptySequence<PsiAnnotation>()
          val previewAnnotation = PREVIEW_ANNOTATION_KEY.get(element)
          if (previewAnnotation != null) {
            annotations += previewAnnotation
          } else {
            if (settings.showExternal) {
              annotations += ExternalAnnotationsManager.getInstance(project).findExternalAnnotations(element)
            }
            if (settings.showInferred) {
              annotations += InferredAnnotationsManager.getInstance(project).findInferredAnnotations(element)
            }
          }

          val shownAnnotations = mutableSetOf<String>()
          val modifierListPresentations = mutableListOf<InlayPresentation>()
          val typePresentations = mutableListOf<InlayPresentation>()
          annotations.forEach { annotation ->
            val nameReferenceElement = annotation.nameReferenceElement
            if (nameReferenceElement != null && element.modifierList != null) {
              if (shownAnnotations.add(nameReferenceElement.qualifiedName) ||
                  JavaDocInfoGenerator.isRepeatableAnnotationType(annotation)) {
                val typeAnno = isTypeAnno(annotation)
                val targetList = if (typeAnno) typePresentations else modifierListPresentations
                targetList.add(createPresentation(annotation, element))
              }
            }
          }
          addModifierListPresentations(modifierListPresentations, element, document)
          addTypePresentations(typePresentations, element)
        }
        return true
      }

      private fun isTypeAnno(annotation: PsiAnnotation): Boolean {
        val qualifiedName = annotation.qualifiedName ?: return false
        val typeAnno = qualifiedName == AnnotationUtil.NOT_NULL ||
                       qualifiedName == AnnotationUtil.NULLABLE ||
                       qualifiedName == AnnotationUtil.UNKNOWN_NULLABILITY ||
                       qualifiedName == Mutability.UNMODIFIABLE_ANNOTATION ||
                       qualifiedName == Mutability.UNMODIFIABLE_VIEW_ANNOTATION
        return typeAnno && PsiUtil.isAvailable(JavaFeature.TYPE_ANNOTATIONS, annotation)
      }

      private fun addTypePresentations(typePresentations: List<InlayPresentation>, element: PsiModifierListOwner) {
        if (typePresentations.isEmpty()) return
        val typeElement = when (element) {
          is PsiVariable -> element.typeElement
          is PsiMethod -> element.returnTypeElement
          else -> null
        } ?: return
        val anchor = typeElement.children.firstOrNull { PsiUtil.isJavaToken(it, arrayTypeStart) } ?: typeElement
        val presentation: InlayPresentation = SequencePresentation(typePresentations)
        val offset = anchor.textRange.startOffset
        sink.addInlineElement(offset, false, factory.inset(presentation, left = 1, right = 1), false)
      }

      private fun addModifierListPresentations(
        modifierListPresentations: List<InlayPresentation>,
        element: PsiModifierListOwner,
        document: Document?,
      ) {
        if (modifierListPresentations.isEmpty()) return
        val offset = element.modifierList?.textRange?.startOffset ?: return
        val presentation = SequencePresentation(modifierListPresentations)
        val prevSibling = element.prevSibling
        when {
          // element is first in line
          prevSibling is PsiWhiteSpace && element !is PsiParameter && prevSibling.textContains('\n') && document != null -> {
            val line = document.getLineNumber(offset)
            val startOffset = document.getLineStartOffset(line)
            val shifted = factory.inset(presentation, left = EditorUtil.textWidth(editor, document.charsSequence, startOffset, offset, Font.PLAIN, 0))
            sink.addBlockElement(offset, true, true, BlockInlayPriority.ANNOTATIONS, shifted)
          }
          else -> {
            sink.addInlineElement(offset, false, factory.inset(presentation, left = 1, right = 1), false)
          }
        }
      }

      private fun collectTypeAnnotations(type: PsiType, anchor: PsiElement, sink: InlayHintsSink) {
        val manager = ExternalAnnotationsManager.getInstance(project)
        val presentations = type.annotations.filter(manager::isExternalAnnotation).map(this::annotationPresentation)
        if (presentations.isNotEmpty()) {
          val offset = anchor.textRange.startOffset
          var presentation: InlayPresentation = SequencePresentation(presentations)
          presentation = MenuOnClickPresentation(presentation, project) {
            listOf(
              ToggleSettingsAction(JavaBundle.message("settings.inlay.java.turn.off.external.annotations"), settings::showExternal, settings)
            )
          }
          sink.addInlineElement(offset, false, factory.inset(presentation, left = 1, right = 1), false)
        }
      }

      private fun createPresentation(
        annotation: PsiAnnotation,
        element: PsiModifierListOwner
      ): MenuOnClickPresentation {
        val presentation = annotationPresentation(annotation)
        return MenuOnClickPresentation(presentation, project) {
          val makeExplicit = InsertAnnotationAction(project, file, element)
          listOf(
            makeExplicit,
            ToggleSettingsAction(JavaBundle.message("settings.inlay.java.turn.off.external.annotations"), settings::showExternal, settings),
            ToggleSettingsAction(JavaBundle.message("settings.inlay.java.turn.off.inferred.annotations"), settings::showInferred, settings)
          )
        }
      }

      private fun annotationPresentation(annotation: PsiAnnotation): InlayPresentation = with(factory) {
        val nameReferenceElement = annotation.nameReferenceElement
        val parameterList = annotation.parameterList

        val presentations = mutableListOf(
          smallText("@"),
          psiSingleReference(smallText(nameReferenceElement?.referenceName ?: "")) { nameReferenceElement?.resolve() }
        )

        parametersPresentation(parameterList)?.let {
          presentations.add(it)
        }
        roundWithBackground(SequencePresentation(presentations))
      }

      private fun parametersPresentation(parameterList: PsiAnnotationParameterList): InlayPresentation? {
        val attributes = parameterList.attributes
        return when {
          attributes.isEmpty() -> null
          else -> insideParametersPresentation(attributes, collapsed = parameterList.textLength > 60)
        }
      }

      private fun insideParametersPresentation(attributes: Array<PsiNameValuePair>, collapsed: Boolean) = with(factory) {
        collapsible(
          smallText("("),
          smallText("..."),
          {
            join(
              presentations = attributes.map { pairPresentation(it) },
              separator = { smallText(", ") }
            )
          },
          smallText(")"),
          collapsed
        )
      }

      private fun pairPresentation(attribute: PsiNameValuePair) = with(factory) {
        when (val attrName = attribute.name) {
          null -> attrValuePresentation(attribute)
          else -> seq(
            psiSingleReference(smallText(attrName), resolve = { attribute.reference?.resolve() }),
            smallText(" = "),
            attrValuePresentation(attribute)
          )
        }
      }

      private fun PresentationFactory.attrValuePresentation(attribute: PsiNameValuePair) =
        smallText(attribute.value?.text ?: "")
    }
  }

  override fun createSettings(): Settings = Settings()

  override val name: String
    get() = JavaBundle.message("settings.inlay.java.annotations")
  override val key: SettingsKey<Settings>
    get() = ourKey

  override fun getCaseDescription(case: ImmediateConfigurable.Case): String? {
    when (case.id) {
      "inferred.annotations" -> return JavaBundle.message("inlay.annotation.hints.inferred.annotations")
      "external.annotations" -> return JavaBundle.message("inlay.annotation.hints.external.annotations")
    }
    return null
  }

  override val previewText: String? = null

  private val PREVIEW_ANNOTATION_KEY = Key.create<PsiAnnotation>("preview.annotation.key")

  override fun preparePreview(editor: Editor, file: PsiFile, settings: Settings) {
    val psiMethod = (file as PsiJavaFile).classes[0].methods[0]
    val factory = PsiElementFactory.getInstance(file.project)
    if (psiMethod.parameterList.isEmpty) {
      PREVIEW_ANNOTATION_KEY.set(psiMethod, factory.createAnnotationFromText("@Deprecated", psiMethod))
    }
    else
      PREVIEW_ANNOTATION_KEY.set(psiMethod.parameterList.getParameter(0), factory.createAnnotationFromText("@NotNull", psiMethod))
  }

  override fun createConfigurable(settings: Settings): ImmediateConfigurable {
    return object : ImmediateConfigurable {
      override fun createComponent(listener: ChangeListener): JComponent = panel {}

      override val mainCheckboxText: String
        get() = JavaBundle.message("settings.inlay.java.show.hints.for")

      override val cases: List<ImmediateConfigurable.Case>
        get() = listOf(
          ImmediateConfigurable.Case(JavaBundle.message("settings.inlay.java.inferred.annotations"), "inferred.annotations", settings::showInferred),
          ImmediateConfigurable.Case(JavaBundle.message("settings.inlay.java.external.annotations"), "external.annotations", settings::showExternal)
        )
    }
  }

  data class Settings(var showInferred: Boolean = false, var showExternal: Boolean = true)


  class ToggleSettingsAction(@NlsActions.ActionText val text: String, val prop: KMutableProperty0<Boolean>, val settings: Settings) : AnAction() {

    override fun update(e: AnActionEvent) {
      e.presentation.text = text
    }

    override fun getActionUpdateThread(): ActionUpdateThread {
      return ActionUpdateThread.BGT
    }

    override fun actionPerformed(e: AnActionEvent) {
      val project = e.project ?: return
      prop.set(!prop.get())
      val storage = InlayHintsSettings.instance()
      storage.storeSettings(ourKey, JavaLanguage.INSTANCE, settings)
      InlayHintsPassFactoryInternal.restartDaemonUpdatingHints(project, "AnnotationInlayProvider.ToggleSettingsAction.actionPerformed")
    }

  }
}

class InsertAnnotationAction(
  private val project: Project,
  private val file: PsiFile,
  private val element: PsiModifierListOwner
) : AnAction() {
  override fun update(e: AnActionEvent) {
    e.presentation.isVisible = file.virtualFile?.isInLocalFileSystem == true
    e.presentation.text = JavaBundle.message("settings.inlay.java.insert.annotation")
  }


  override fun getActionUpdateThread(): ActionUpdateThread {
    return ActionUpdateThread.BGT
  }

  override fun actionPerformed(e: AnActionEvent) {
    val intention = MakeInferredAnnotationExplicit()
    if (intention.isAvailable(file, element)) {
      intention.makeAnnotationsExplicit(project, file, element)
    }
  }
}

private val ourKey: SettingsKey<AnnotationInlayProvider.Settings> = SettingsKey("annotation.hints")